// ffs/wizard module
import ffs_free from "./ffs/free";
import ffs_parser from "./ffs/ffs.pegjs";

var ffs = {};
var freeFormQuery;

/* this converts a random boolean expression into a normalized form:
 * A∧B∧… ∨ C∧D∧… ∨ …
 * for example: A∧(B∨C) ⇔ (A∧B)∨(A∧C)
 */
function normalize(query) {
  var normalized_query = {
    logical: "or",
    queries: []
  };
  function normalize_recursive(rem_query) {
    if (!rem_query.logical) {
      return [
        {
          logical: "and",
          queries: [rem_query]
        }
      ];
    } else if (rem_query.logical === "and") {
      var c1 = normalize_recursive(rem_query.queries[0]);
      var c2 = normalize_recursive(rem_query.queries[1]);
      // return cross product of c1 and c2
      var c = [];
      for (var i = 0; i < c1.length; i++)
        for (var j = 0; j < c2.length; j++) {
          c.push({
            logical: "and",
            queries: c1[i].queries.concat(c2[j].queries)
          });
        }
      return c;
    } else if (rem_query.logical === "or") {
      var c1 = normalize_recursive(rem_query.queries[0]);
      var c2 = normalize_recursive(rem_query.queries[1]);
      return [].concat(c1, c2);
    } else {
      alert("unsupported boolean operator: " + rem_query.logical);
    }
  }
  normalized_query.queries = normalize_recursive(query);
  return normalized_query;
}

function escRegexp(str) {
  return str.replace(/([()[{*+.$^\\|?])/g, "\\$1");
}

ffs.construct_query = function (search, comment, callback) {
  function quote_comment_str(s) {
    // quote strings that are to be used within c-style comments
    // replace any comment-ending sequences in these strings that would break the resulting query
    return s.replace(/\*\//g, "[…]").replace(/\n/g, "\\n");
  }

  try {
    ffs = ffs_parser.parse(search);
  } catch (e) {
    console.warn("ffs parse error", e);
    return callback("ffs parse error");
  }

  var query_parts = [];
  var bounds_part;

  query_parts.push("/*");
  if (comment) {
    query_parts.push(comment);
  } else {
    query_parts.push("This has been generated by the overpass-turbo wizard.");
    query_parts.push("The original search was:");
    query_parts.push("“" + quote_comment_str(search) + "”");
  }
  query_parts.push("*/");
  query_parts.push("[out:json][timeout:25];");

  switch (ffs.bounds) {
    case "area":
      query_parts.push("// fetch area “" + ffs.area + "” to search in");
      query_parts.push("{{geocodeArea:" + ffs.area + "}}->.searchArea;");
      bounds_part = "(area.searchArea)";
      break;
    case "around":
      query_parts.push("// adjust the search radius (in meters) here");
      query_parts.push("{{radius=1000}}");
      bounds_part = "(around:{{radius}},{{geocodeCoords:" + ffs.area + "}})";
      break;
    case "bbox":
      bounds_part = "({{bbox}})";
      break;
    case "global":
      bounds_part = undefined;
      break;
    default:
      alert("unknown bounds condition: " + ffs.bounds);
      return false;
      break;
  }

  function get_query_clause(condition) {
    function esc(str) {
      if (typeof str !== "string") return;
      // see http://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#Escaping
      return str
        .replace(/\\/g, "\\\\")
        .replace(/"/g, '\\"') // need to escape those
        .replace(/\t/g, "\\t")
        .replace(/\n/g, "\\n"); // also escape newlines an tabs for better readability of the query
    }
    var key = esc(condition.key);
    var val = esc(condition.val);
    // convert substring searches into matching regexp ones
    if (condition.query === "substr") {
      condition.query = "like";
      condition.val = {regex: escRegexp(condition.val)};
    }
    // special case for empty values
    // see https://github.com/drolbr/Overpass-API/issues/53
    if (val === "") {
      if (condition.query === "eq") {
        condition.query = "like";
        condition.val = {regex: "^$"};
      } else if (condition.query === "neq") {
        condition.query = "notlike";
        condition.val = {regex: "^$"};
      }
    }
    // special case for empty keys
    // see https://github.com/drolbr/Overpass-API/issues/53#issuecomment-26325122
    if (key === "") {
      if (condition.query === "key") {
        condition.query = "likelike";
        key = "^$";
        condition.val = {regex: ".*"};
      } else if (condition.query === "eq") {
        condition.query = "likelike";
        key = "^$";
        condition.val = {regex: "^" + escRegexp(condition.val) + "$"};
      } else if (condition.query === "like") {
        condition.query = "likelike";
        key = "^$";
      }
    }
    // construct the query clause
    switch (condition.query) {
      case "key":
        return '["' + key + '"]';
      case "nokey":
        return '["' + key + '"!~".*"]';
      case "eq":
        return '["' + key + '"="' + val + '"]';
      case "neq":
        return '["' + key + '"!="' + val + '"]';
      case "like":
        return (
          '["' +
          key +
          '"~"' +
          esc(condition.val.regex) +
          '"' +
          (condition.val.modifier === "i" ? ",i" : "") +
          "]"
        );
      case "likelike":
        return (
          '[~"' +
          key +
          '"~"' +
          esc(condition.val.regex) +
          '"' +
          (condition.val.modifier === "i" ? ",i" : "") +
          "]"
        );
      case "notlike":
        return (
          '["' +
          key +
          '"!~"' +
          esc(condition.val.regex) +
          '"' +
          (condition.val.modifier === "i" ? ",i" : "") +
          "]"
        );
      case "meta":
        switch (condition.meta) {
          case "id":
            return "(" + val + ")";
          case "newer":
            if (
              condition.val.match(
                /^-?\d+ ?(seconds?|minutes?|hours?|days?|weeks?|months?|years?)?$/
              )
            )
              return '(newer:"{{date:' + val + '}}")';
            return '(newer:"' + val + '")';
          case "user":
            return '(user:"' + val + '")';
          case "uid":
            return "(uid:" + val + ")";
          default:
            console.log("unknown query type: meta/" + condition.meta);
            return false;
        }
      case "free form":
      // own module, special cased below
      default:
        console.log("unknown query type: " + condition.query);
        return false;
    }
  }
  function get_query_clause_str(condition) {
    function quotes(s) {
      if (s.match(/^[a-zA-Z0-9_]+$/) === null)
        return '"' + s.replace(/"/g, '\\"') + '"';
      return s;
    }
    function quoteRegex(s) {
      if (s.regex.match(/^[a-zA-Z0-9_]+$/) === null || s.modifier)
        return "/" + s.regex.replace(/\//g, "\\/") + "/" + (s.modifier || "");
      return s.regex;
    }
    switch (condition.query) {
      case "key":
        return quote_comment_str(quotes(condition.key) + "=*");
      case "nokey":
        return quote_comment_str(quotes(condition.key) + "!=*");
      case "eq":
        return quote_comment_str(
          quotes(condition.key) + "=" + quotes(condition.val)
        );
      case "neq":
        return quote_comment_str(
          quotes(condition.key) + "!=" + quotes(condition.val)
        );
      case "like":
        return quote_comment_str(
          quotes(condition.key) + "~" + quoteRegex(condition.val)
        );
      case "likelike":
        return quote_comment_str(
          "~" + quotes(condition.key) + "~" + quoteRegex(condition.val)
        );
      case "notlike":
        return quote_comment_str(
          quotes(condition.key) + "!~" + quoteRegex(condition.val)
        );
      case "substr":
        return quote_comment_str(
          quotes(condition.key) + ":" + quotes(condition.val)
        );
      case "meta":
        switch (condition.meta) {
          case "id":
            return quote_comment_str("id:" + quotes(condition.val));
          case "newer":
            return quote_comment_str("newer:" + quotes(condition.val));
          case "user":
            return quote_comment_str("user:" + quotes(condition.val));
          case "uid":
            return quote_comment_str("uid:" + quotes(condition.val));
          default:
            return "";
        }
      case "free form":
        return quote_comment_str(quotes(condition.free));
      default:
        return "";
    }
  }

  ffs.query = normalize(ffs.query);

  var freeForm = false;
  for (var i = 0; i < ffs.query.queries.length; i++) {
    var and_query = ffs.query.queries[i];
    for (var j = 0; j < and_query.queries.length; j++) {
      var cond_query = and_query.queries[j];
      if (cond_query.query === "free form") {
        freeForm = true;
        break;
      }
    }
  }

  // if we have a "free form" query part, need to load it before first use:
  (freeForm
    ? ffs_free
    : function (x) {
        x(null);
      })(function (freeFormQuery) {
    query_parts.push("// gather results");
    query_parts.push("(");
    for (var i = 0; i < ffs.query.queries.length; i++) {
      var and_query = ffs.query.queries[i];

      var types = ["node", "way", "relation"];
      var clauses = [];
      var clauses_str = [];
      for (var j = 0; j < and_query.queries.length; j++) {
        var cond_query = and_query.queries[j];
        // todo: looks like some code duplication here could be reduced by refactoring
        if (cond_query.query === "free form") {
          var ffs_clause = freeFormQuery.get_query_clause(cond_query);
          if (ffs_clause === false) return callback("unknown ffs string");
          // restrict possible data types
          types = types.filter(function (t) {
            return ffs_clause.types.indexOf(t) != -1;
          });
          // add clauses
          clauses_str.push(get_query_clause_str(cond_query));
          clauses = clauses.concat(
            ffs_clause.conditions.map(function (condition) {
              return get_query_clause(condition);
            })
          );
        } else if (cond_query.query === "type") {
          // restrict possible data types
          types = types.indexOf(cond_query.type) != -1 ? [cond_query.type] : [];
        } else {
          // add another query clause
          clauses_str.push(get_query_clause_str(cond_query));
          var clause = get_query_clause(cond_query);
          if (clause === false) return false;
          clauses.push(clause);
        }
      }
      clauses_str = clauses_str.join(" and ");

      // construct query
      query_parts.push("  // query part for: “" + clauses_str + "”");
      for (var t = 0; t < types.length; t++) {
        var buffer = "  " + types[t];
        for (var c = 0; c < clauses.length; c++) buffer += clauses[c];
        if (bounds_part) buffer += bounds_part;
        buffer += ";";
        query_parts.push(buffer);
      }
    }
    query_parts.push(");");

    query_parts.push("// print results");
    query_parts.push("out body;");
    query_parts.push(">;");
    query_parts.push("out skel qt;");

    callback(null, query_parts.join("\n"));
  });
};

// this is a "did you mean …" mechanism against typos in preset names
ffs.repair_search = function (search, callback) {
  try {
    ffs = ffs_parser.parse(search);
  } catch (e) {
    return callback(false);
  }

  function quotes(s) {
    if (s.match(/^[a-zA-Z0-9_]+$/) === null)
      return '"' + s.replace(/"/g, '\\"') + '"';
    return s;
  }

  var search_parts = [];
  var repaired = false;

  ffs_free(function (freeFormQuery) {
    ffs.query = normalize(ffs.query);
    ffs.query.queries.forEach(function (q) {
      q.queries.forEach(validateQuery);
    });
    function validateQuery(cond_query) {
      if (cond_query.query === "free form") {
        var ffs_clause = freeFormQuery.get_query_clause(cond_query);
        if (ffs_clause === false) {
          // try to find suggestions for occasional typos
          var fuzzy = freeFormQuery.fuzzy_search(cond_query);
          var free_regex = null;
          try {
            free_regex = new RegExp(
              "['\"]?" + escRegexp(cond_query.free) + "['\"]?"
            );
          } catch (e) {}
          if (fuzzy && search.match(free_regex)) {
            search_parts = search_parts.concat(search.split(free_regex));
            search = search_parts.pop();
            var replacement = quotes(fuzzy);
            search_parts.push(replacement);
            repaired = true;
          }
        }
      }
    }
    search_parts.push(search);

    if (!repaired) callback(false);
    else callback(search_parts);
  });
};

ffs.invalidateCache = function () {
  freeFormQuery = undefined;
};

export default ffs;
